β

Как стать автором
Погнали в космос?
Какой вы ML-специалист: составим портреты типичных ML’щиков

Моя лентаВсе потоки

alivewel 5 ноя 2024 в 17:42

Погружение в интерфейсы Go

Средний

13 мин

23K

Go*Программирование*ООП*Интерфейсы*

Туториал

Интерфейсы — одна из самых сложных тем для начинающих в Go. Я решил тщательно разобраться с этой темой и одновременно написать эту статью. После прочтения этой статьи вы сможете ответить на следующие вопросы:

Что такое интерфейс?

Что такое значение интерфейса?

Что такое пустой интерфейс?

Почему nil-интерфейс не равен nil?

Где разместить интерфейс?

Какая структура интерфейса?

Эта статья поможет вам лучше понять, как работают интерфейсы в Go и как их правильно использовать в вашем коде.

Что такое интерфейс?

Интерфейс в языке Go — это специальный тип, который определяет набор сигнатур методов, но не содержит их реализацию. Интерфейсы позволяют описывать поведение типов, что делает код более гибким. Они добавляют абстракцию, позволяя работать с различными типами, не заботясь о конкретных реализациях.

Объявление интерфейса

Чтобы объявить интерфейс, используется ключевое слово type, затем указывается имя интерфейса и ключевое слово interface, после чего в фигурных скобках перечисляются сигнатуры методов, которые должны быть реализованы типами, удовлетворяющими этому интерфейсу. Например:

type MyInterface interface {
MyMethod()
}

Встраивание интерфейсов

В Go интерфейсы поддерживает встраивание. Для этого нужно в объявлении нового интерфейса указать название какого-то другого интерфейса.

type I interface {
MyInterface
}

Значение интерфейса

Значение интерфейса можно рассматривать как кортеж (пару) из значения и конкретного типа:

(value, type)

Тип: Это конкретный тип данных, к которому принадлежит значение. Например, это может быть строка (string), число (int), структура (struct) или любой другой тип. Тип определяет, какие методы доступны для вызова на этом значении.

Значение: Это конкретное значение, которое принадлежит определенному базовому типу. Например, это может быть строка "Hello", число 123 или экземпляр структуры, такой как Dog.

Пример создания значения интерфейсного типа:

Создадим интерфейс Animal, который требует реализации метода Speak().

type Animal interface {
Speak() string
}

Затем создадим переменную animal интерфейсного типа Animal:

var animal Animal

Через конструкцию fmt.Printf("Value %v, type %T\n", animal, animal) выведем на экран значение и тип интерфейса animal. Увидим следующие сообщение:

Value <nil>, type <nil>

Здесь мы видим, что value и type равны nil. Это означает, что переменная animal не содержит никакого значения и не указывает на конкретный тип.

При попытке сравнить интерфейс с nil мы видим, что сообщение "animal is not nil" не распечатается.

if animal != nil {
fmt.Println("animal is not nil")
}

Создадим указатель на новый объект Dog и присвоим dog интерфейсу animal.

dog := &Dog{}
animal = dog

Теперь, когда animal указывает на объект Dog, мы можем вызвать метод Speak. Это безопасно, потому что Dog реализует метод Speak, требуемый интерфейсом Animal.

animal.Speak() // OK

Снова воспользуемся конструкцией fmt.Printf("Value %v, type %T\n", animal, animal) выведем на экран значение и тип интерфейса. Увидим следующие сообщение:

Value &{}, type *main.Dog

Тип: %T показывает, что animal — это указатель на Dog (*main.Dog).

Затем мы проверяем, не равна ли animal nil:

if animal != nil {
fmt.Println("animal is not nil")
}

Поскольку animal указывает на объект Dog, условие выполняется, и выводится сообщение "animal is not nil".

Изменим поле Name у объекта Dog, на который указывает dog

dog.Name = "Шайтан"

Выведем снова через Printf тип и значение переменной animal:

Value &{Шайтан}, type *main.Dog

Теперь значение интерфейса поля Name обновлено до "Шайтан", что видно в выводе.

Вызов метода интерфейсного типа

Значение интерфейсного типа != nil, когда конкретный тип != nil. Мы можем безопасно вызвать метод у интерфейса только в случае, когда значение интерфейсного типа != nil. В противном случае при вызове метода мы словим панику.

var animal Animal
animal.Speak() // паника при попытке вызвать (interface == nil)

dog := &Dog{}
animal = dog // interface != nil
animal.Speak() // OK

Описание структуры интерфейса

Структура iface

type iface struct {
tab *itab // это указатель на Interface Table или itable - структуру, которая хранит некоторые метаданные о типе и список методов, используемых для удовлетворения интерфейса.
data unsafe.Pointer // хранимые данные (указатель на значение)
}

tab *itab: Это указатель на таблицу интерфейса (itable), которая содержит информацию о типе и методах, необходимых для реализации интерфейса. Эта таблица помогает Go определить, какие методы доступны для данного интерфейсного значения и как их вызывать. (описание см. ниже)

data unsafe.Pointer: Это указатель на конкретные данные или значение, которые реализуют интерфейс. Использование unsafe.Pointer позволяет интерфейсу ссылаться на данные произвольного типа, сохраняя при этом информацию о том, как к ним обращаться через itab.

Структура itab

type itab struct { // 40 bytes on a 64bit arch
inter *interfacetype // тип интерфейса
_type *_type // все, что мы знаем про тип из которого образован элемент интерфейса
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // методы, которые должна описывать структура, чтобы релизовывать интерфейс
}

inter *interfacetype: Метаданные интерфейса.

_type *_type: Указатель на информацию о конкретном типе, который реализует интерфейс. Это позволяет Go знать, как обращаться с данными, которые реализуют интерфейс.

hash uint32: Хеш типа, который используется для оптимизации операций с интерфейсами, таких как type switches.

fun [1]uintptr: Массив указателей на функции, которые должны быть реализованы для удовлетворения интерфейса. Это позволяет динамически вызывать методы на интерфейсных значениях. uintptr - целочисленное представление адреса в памяти, указатель на первый элемент массива, который содержит указатели на методы. Размер массива [1], чтобы сохранить указатель на первый элемент массива.

Иллюстрация хранения значений интерфейсного типа в структуре интерфейса:

Создадим свой пользовательский тип Binary с двумя методами String() string и Get() uint64.

type Binary uint64

func (i Binary) String() string {
return strconv.Uitob64(i.Get(), 2)
}

func (i Binary) Get() uint64 {
return uint64(i)
}

Создадим экземпляр структуры Binary и присвоим ему значение:

b := Binary(200)

Значение интерфейса представлены в виде пары из двух машинных слов, дающей указатель на информацию о типе, хранящемся в интерфейсе, и указатель на связанные данные.

Первое слово в значении интерфейса указывает на таблицу интерфейсов itable. В нем хранится информация о конкретном типе type и списке указателей на методы fun[0]. В нашем случае type- Binary, методы String() string и Get() uint64.

Второе слово указывает на значение data. В нашем случае data - 200.

Через конструкцию fmt.Printf("Value %v, type %T\n", num, num) выведем на экран значение и тип интерфейса. Получим:

Value 11001000, type main.Binary

Вопрос с собеседования

Обладая этими знаниями, вы сможете ответить на популярный вопрос из собеседования:

Что выведет программа?

func main() {
var ptr *struct{}
var iface interface{}
iface = ptr
if iface == nil {
println("It's nil!")
}
}

Пустой интерфейс

Пустой интерфейс - это интерфейс, у которого отсутствуют методы. Для имплементации интерфейса нужно реализовать всего его методы. Для имплементации пустого интерфейса не нужно реализовывать никаких методов. Соответственно, любой тип в Go имплементирует пустой интерфейс. В других языках программирования такое называется any. В Go тоже есть any, это алиас (пользовательский тип) на пустой интерфейс.

При создании переменной пустого интерфейса в дальнейшем мы можем присвоить ему любой тип.

var emptyInterface interface{}

emptyInterface = dog

emptyInterface = 123

emptyInterface = true

Цитата про пустой интерфейс из статьи про постулаты Go (Go proverbs)

Пустой интерфейс ни о чём не говорит (interface{} says nothing)

Этот постулат говорит о том, что интерфейсы — «поведенческие типы» — должны что-то означать. Если вы создаёте интерфейс, это что-то означает и служит конкретной цели. Пустой же интерфейс (interface{}) ничего не означает и ни о чём не говорит.

Есть ситуации, когда его нужно использовать, но они чаще исключение — не используйте interface{} без повода. Новички часто переиспользуют пустые интерфейсы, и масса вопросов на Stack Overflow именно о них.

Неявная имплементация интерфейсов

В Go используется неявная имплементация интерфейсов. В других языках программирования требуется использование ключевого слово implements. В Go используется концепция утиной типизации. Для того, чтобы имплементировать интерфейс типу необходимо реализовать все его методы. При этом можно реализовать больше методов чем это требуется, но меньше нельзя. Один тип может имплементировать несколько интерфейсов.

Полиморфизм

Полиморфизм — это концепция, позволяющая объектам разных типов быть обработанными через единый интерфейс. В данном примере полиморфизм проявляется в том, что функция MakeAnimalSpeak может принимать любой тип, который реализует интерфейс Animal, и вызывать метод Speak, не зная конкретного типа объекта. Это позволяет писать более гибкий и расширяемый код, так как вы можете добавлять новые типы, реализующие интерфейс Animal, без необходимости изменять существующий код, который работает с этим интерфейсом. С помощью интерфейсов мы можем обстрагироваться от конкретных типов.

Пример

Обе структуры Dog и Cat реализуют метод Speak, что делает их совместимыми с интерфейсом Animal. Метод Speak возвращает строку, описывающую звук, который издает животное.

Функция MakeAnimalSpeak принимает параметр типа Animal. Поскольку Dog и Cat реализуют интерфейс Animal, они могут быть переданы в эту функцию. Внутри функции вызывается метод Speak, который возвращает строку, и эта строка выводится на экран. Aункция MakeAnimalSpeak принимает параметр типа Animal. Поскольку Dog и Cat реализуют интерфейс Animal, они могут быть переданы в эту функцию. Внутри функции вызывается метод Speak, который возвращает строку, и эта строка выводится на экран.

type Animal interface {
Speak() string
}

type Dog struct {
Name string
}

func (d Dog) Speak() string {
return fmt.Sprintf("Собака %s лает", d.Name)
}

type Cat struct {
Name string
}

func (c Cat) Speak() string {
return fmt.Sprintf("Кошка %s мяукает", c.Name)
}

// Функция, принимающая интерфейс Animal и вызывающая метод Speak
func MakeAnimalSpeak(a Animal) {
fmt.Println(a.Speak())
}

func main() {
dog := Dog{Name: "Шайтан"}
cat := Cat{Name: "Тайсон"}

// Вызов функции MakeAnimalSpeak для каждого животного
MakeAnimalSpeak(dog)
MakeAnimalSpeak(cat)
}

Type Assertion

Type Assertion позволяет нам вызывать только те методы, которые существуют у данного конкретного типа. Она возвращает две переменные: значение конкретного типа и флаг, указывающий на успешность преобразования значения интерфейсного типа в конкретный тип. После успешного преобразования мы можем вызывать методы, специфичные для этого конкретного типа.

Для каждой структуры Dog и Cat мы добавили уникальные методы:

func (d Dog) Bark() string {
return fmt.Sprintf("%s громко лает!", d.Name)
}

func (c Cat) Purr() string {
return fmt.Sprintf("%s мурлычет.", c.Name)
}

Создадим функцию processAnimalTypeAssertion, которая принимает на вход интерфейс Animal и выполняет type assertion для проверки конкретного типа, чтобы вызвать его уникальные методы:

func processAnimalTypeAssertion(animal Animal) {
if dog, ok := animal.(*Dog); ok {
fmt.Printf("Type: %T Value: %#v\n", dog, dog)
fmt.Println(dog.Bark())
}
if cat, ok := animal.(*Cat); ok {
fmt.Printf("Type: %T Value: %#v\n", cat, cat)
fmt.Println(cat.Purr())
}
}

Вызовем функцию processAnimalTypeAssertion в main.

func main() {
dog := &Dog{Name: "Шайтан"}
cat := &Cat{Name: "Тайсон"}

processAnimalTypeAssertion(dog)
processAnimalTypeAssertion(cat)
}

При выполнении этого кода получим следующий вывод:

Type: *main.Dog Value: &main.Dog{Name:"Шайтан"}
Шайтан громко лает!
Type: *main.Cat Value: &main.Cat{Name:"Тайсон"}
Тайсон мурлычет.

Type Switch

Type Switch предоставляет синтаксический сахар для работы с Type Assertion. Таким образом можем заменить функцию processAnimalTypeAssertion функцией processAnimalTypeSwitch:

func processAnimalTypeSwitch(animal Animal) {
switch v := animal.(type) {
case *Dog:
fmt.Printf("Type: %T Value: %#v\n", v, v)
fmt.Println(v.Bark())
case *Cat:
fmt.Printf("Type: %T Value: %#v\n", v, v)
fmt.Println(v.Purr())
default:
fmt.Printf("Type: %T Value: %#v\n", v, v)
}
}

nil-интерфейсы в Go

В Go очень интересно ведут себя nil-интерфейсы. Это один из 50 подводных камней, которые неочевидны при изучении языка.

Мы можем создать переменную пустого интерфейса, и при сравнении с nil, мы получаем true, что означает interface == nil. Далее мы можем создать указатель на структуру и снова сравнить его с nil, мы получаем true. Все логично.

Теперь присвоим переменной интерфейса указатель на структуру. Теперь мы получаем false. Почему так происходит? После присвоения у интерфейса указателя на структуру, в интерфейсе записывается конкретный тип, это значит, что значение интерфейса уже не равно nil. Поэтому при сравнении мы получаем false.

package main

import "fmt"

type Animal interface {
Speak() string
}

type Dog struct {
Name string
}

func main() {
var i interface{}
// type == nil, value == nil
// поэтому i == nil
fmt.Println(i == nil) // true

var d *Dog
fmt.Println(d == nil) // true

i = d
// type == *Dog, value == nil
// поскольку type != nil, то i != nil
fmt.Println(i == nil) // false
}

Где лучше размещать интерфейс?

Данная секция разработана на основе видео-ролика Николая Тузова, в котором он подробно и понятно разобрал данную тему.

Небольший спойлер: Интерфейсы лучше размещать в месте их использования.

Рекомендации по использованию интерфейсов:

Интерфейсы должны быть минималистичными.

Интерфейс ничего не должен знать о типах, которые его реализуют.

Рассмотрим пример некого сервиса. В этом сервисе нас интересует два слоя: storage и handlers. В слое storage есть пакет users, в котором указаны методы для различных БД: Postgres, Redis, MySQL и т.д.

Структура проекта

some_service/: Главная директория сервиса.

handlers/: Папка для хэндлеров (обработчиков).

userinfo/: Подпапка для работы с информацией о пользователях.

lib/: Библиотеки и вспомогательные модули.

services/: Логика и бизнес-слой сервиса.

storage/: Хранилище данных.

users/: Папка для управления пользователями.

Подпапки для различных реализаций хранения, такие как:

cache/

mysql/

postgres/

redis/

users.go: Файл с реализацией интерфейсов для работы с пользователями.

Чтобы не зависеть от типа реализации мы решили описать общий интерфейс, который называется Storage. В общем интерфейсе содержаться все методы, которые необходимы для взаимодействия со всеми БД.

package users

type User struct {
ID int
Name string
Age int
}

type Storage interface {
Users() ([]User, error)
UsersByAge(age int) ([]User, error)
User(id int) (User, error)
Create(user User) error
Update(user User) error
Delete(id int) error
// другие методы...
}

Кажется, что мы соблюдаем правило №2 - интерфейс ничего не должен знать о типах, которые его реализуют, но это не совсем так.

Мы используем данный интерфейс в слой handlers. В частности, в нем у нас есть функция New, которая принимает интерфейс Storage, в котором множество методов.

package userinfo

import (
"context"
"fmt"
"service/handlers"
"service/storage/users"
)

func New(userRepo users.Storage) handlers.Handler {
return func(ctx context.Context) error {
// Получение UID из запроса
uid := 1
user, err := userRepo.User(uid)
if err != nil {
// Обработка ошибки
return fmt.Errorf("failed to get user: %w", err)
}
fmt.Printf("User: %+v\n", user)
return nil
}
}

Чтобы не тащить за собой огромный интерфейс с кучей методов, мы можем описать интерфейс в месте его использования, прямо в этом хендлере. В этом хендлере мы используем один единственный метод User(). Это значит, что мы можем создать здесь интерфейс, в котором будет необходимый метод. Создадим интерфейс UserProvider, в котором будет метод User().

package userinfo

import (
"context"
"fmt"
"service/handlers"
"service/storage/users"
)

type UserProvider interface {
User(int) (users.User, error)
}

func New(userProvider UserProvider) handlers.Handler {
return func(ctx context.Context) error {
// Получение UID из запроса
uid := 1
user, err := userProvider.User(uid)
if err != nil {
// Обработка ошибки
return fmt.Errorf("failed to get user: %w", err)
}
fmt.Printf("User: %+v\n", user)
return nil
}
}

Что нам это дает?

Минималистичный интерфейс: В нашем методе нет намека на какую-либо базу данных. Метод User() просто каким-то образом возвращает пользователя.

Уменьшение связности: Пакет handlers никак не зависит от пакета storage. Связность компонентов системы должна быть как можно меньше.

Понятность кода: Мы сделали ясными ожидания и потребности разных частей системы. При чтении кода в пакете handlers, мы видим, какой интерфейс ожидает функция, и этот интерфейс описан в этом же пакете. При использовании большого интерфейса Storage, мы видим, что у него много методов, и не сразу понятно, для чего они нужны. Также, чтобы прочитать описание интерфейса, нужно перейти в другой пакет.

Гибкость системы: Допустим, мы хотим передать вместо сущности Postgres сущность Redis. Чтобы соответствовать данному интерфейсу, нам придется реализовать все его методы, даже если они не используются.

Тестирование: При написании юнит-тестов для тестирования логики функции нам необходимо изолироваться от какой-либо базы данных. Mock'и позволяют нам этого добиться. Поскольку интерфейс описан в пакете handlers, то и сгенерировать mock мы можем в этом же пакете.

Минусы подхода

Дублирование описания интерфейса по всем частям сервиса. Если мы захотим изменить сигнатуры методов, то нам придется это сделать во всех частях системы. Если бы у нас был один общий интерфейс, достаточно было бы изменить его только в одном месте.

Новичкам из других языков не всегда понятен такой подход, который связан с утиной типизацией и неявной имплементацией интерфейсов в Go.

Принципы SOLID

Также хочется отметить, что придерживаясь такого подхода, мы соответствуем следующим принципам SOLID:

Принцип разделения интерфейсов (I): Программные сущности не должны зависеть от методов, которые они не используют.
Разделение одного большого интерфейса на несколько мелких.

Принцип инверсии зависимостей (D): Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей, детали должны зависеть от абстракций.
Создание абстракций, которые позволяют модулям взаимодействовать без прямой зависимости друг от друга.

Цитата про необходимость разделения одного большого интерфейса на несколько мелких из статьи про постулаты Go (Go proverbs)

Чем больше интерфейс, тем слабее абстракция (The bigger the interface, the weaker the abstraction)

Новички в Go, особенно пришедшие с Java, часто считают, что интерфейсы должны быть большими и содержать много методов. Также часто их смущает неявное удовлетворение интерфейсов. Но самое важное в интерфейсах не это, а культура вокруг них, которая отражена в этом постулате. Чем меньше интерфейс, тем более он полезен. Пайк шутит, что три самых полезных интерфейса, которые он написал — io.Reader, io.Writer и interface{} — на троих в среднем имеют 0.666 метода.

Полезные материалы

Почему интерфейсы лучше размещать в месте использования - GoLang best practices | Николай Тузов

The Go Programming Language Specification

Effective Go - interfaces

Go Data Structures: Interfaces

Go Proverbs

Practical SOLID in Golang

Заключение

В этой статье я собственноручно сделал транскрибацию замечательного ролика Николая Тузова. В своем видео он подробно отвечает на вопрос: "Куда положить интерфейс?". Перед публикацией я списался с Николаем, и он не против использования своего материала.

Эта статья представляет собой тщательно собранный сборник полезного материала о интерфейсах, являющийся результатом въедливого ресерча. В ней содержится множество отсылок и ссылок на другие источники. Я постарался собрать максимум информации, которая может быть полезна при подготовке к собеседованию на тему интерфейсов.

Все примеры можно найти в моем GitHub, а подробное описание доступно на YouTube.

Если вам нравится, как и о чем я пишу – то буду благодарен за подписку на мой ТГ-канал Go Alive (так вы точно не пропустите новые статьи).

Теги:

Go

golang

interface

ооп

пустой интерфейс

interfaces

интерфейс

nil интерфейс

структура интерфейса

куда положить интерфейс

Хабы:

Go

Программирование

ООП

Интерфейсы

+23

164

7
+7

14

Карма

0

Рейтинг

@alivewel

Gopher 100%

Подписаться

Комментарии 7
+7

Публикации

Лучшие за суткиПохожие

A_Tsymbalyuk 13 часов назад

Физика ошибок не прощает. Она догонит тебя на крыше и заставит платить (дважды)

Простой

6 мин

8.1K

Кейс

+40

35

25
+25

BabayMazay 12 часов назад

Самодельные французские радиолампы 1920 г. Часть 3. Разрядная трубка, лампа накаливания

Средний

9 мин

1.5K

Ретроспектива

+33

9

11
+11

Bright_Translate 8 часов назад

Как я от нечего делать замедлил Postgres в 42 000 раз

Средний

11 мин

2.6K

Кейс

Перевод

+27

20

2
+2

butivskiydm 11 часов назад

Моё мини-исследование про продолжительность жизни в России

Простой

2 мин

6.4K

Мнение

+22

17

60
+60

lokosst 13 часов назад

Выделенные серверы + Kubernetes: практическое руководство

17 мин

2.4K

Туториал

+21

23

0

JetHabr 11 часов назад

«Плохие парни работают просто»: пентестеры разбирают реальные кейсы

Средний

9 мин

2.1K

Кейс

+17

19

1
+1

k0mar0v 14 часов назад

HTC Desire HD: история, дизайн и возможности легендарного смартфона

6 мин

2.6K

+17

5

5
+5

MaFrance351 13 часов назад

Домофон Лидер-М. Как скрестить трубку с ключом и всем проиграть

Простой

4 мин

2.5K

+16

5

6
+6

psygalaxy 13 часов назад

Как мы ускорили проверку документации с помощью AI-агента: от боли к решению

Средний

25 мин

1.8K

Обзор

+16

30

9
+9

kubelet 16 часов назад

DNS-хаос, зомби-поды и майнеры в кластере: самые невероятные случаи при работе с Kubernetes

Простой

14 мин

1.2K

Кейс

+16

23

1
+1

Красим папки и распределяем роли: тестируем новую версию менеджера паролей Пассворк 7.0
Турбо

Показать еще

Минуточку внимания

Событие

Как построить IT-бренд через вирусный контент и юмор

Турбо

Полный свайп: оцени, подходит ли тебе работа в IT-ритейле

Промо

Неважно, что ты выберешь — с Промокодусом будет дешевле

Курсы

Event Storming как техника моделирования предметной области и выявления микросервисов
19 августа 2025Systems Education

DDD, ООП и UML для аналитика
30 сентября 2025BABOKSchool

Методы описания бизнес-процессов (IDEF, DFD, BPMN, EPC, UML)
7 октября 2025BABOKSchool

Задачи по программированию на языке C++
По желаниюStepik

Программа курсов: Бэкенд-разработка на Python
По желаниюStepik

Больше курсов на Хабр Карьере

Читают сейчас

Как превратить ноутбук в полноценное рабочее место не хуже стационарного ПК

12K

71
+71

Нашел ЛЯП в игре 5букв от Т-банка, который позволяет со 100% вероятностью угадать скрытое слово

54K

115
+115

Как выйти из IT?.. и пойти слесарем на завод. Моя попыточка дауншифтинга [4\5]

1.5K

12
+12

Моё мини-исследование про продолжительность жизни в России

6.4K

60
+60

GPT-5 получит модуль для борьбы с «галлюцинациями» — The Information

725

3
+3

Красим папки и распределяем роли: тестируем новую версию менеджера паролей Пассворк 7.0
Турбо

Истории

История Яндекс Почты

С Днём сисадмина!

Как расти в ИТ: советы, гайды и опыт сеньоров

1 055 450 очков в облаке – попробуй обогнать

Годнота из блогов компаний

Ближайшие события

7 августа

Мем-пати: как построить IT-бренд через вирусный контент и юмор

Москва

Больше событий в календаре

Маркетинг

13 августа

Как повысить эффективность врача в стоматологии на 25% с помощью ИИ?

Онлайн

Больше событий в календаре

Менеджмент

13 августа

Тест-драйв Хабра для маркетологов и пиарщиков

Москва

Больше событий в календаре

Маркетинг

16 августа

«ИТ-летник» — конференция от Совкомбанк Технологий

Казань

Больше событий в календаре

Разработка

Аналитика

3 сентября

«GoCloud Tech 2025» — IT-конференция про облака и AI

Москва • Онлайн

Больше событий в календаре

Разработка

18 – 19 сентября

Конференция по корпоративному онлайн-обучению iSpring Days 2025

Москва • Онлайн

Больше событий в календаре

Администрирование

Менеджмент

Другое

19 сентября

Премия для технологических стартапов Startech.Awards 2025

Москва • Онлайн

Больше событий в календаре

Другое

30 сентября

VII премия в сфере фармацевтического маркетинга и креатива MedMen Awards 2025

Онлайн

Больше событий в календаре

Маркетинг

Другое

30 октября

Премия «Проксима»

Москва

Больше событий в календаре

Маркетинг

Реклама

АО СЕЛЕКТЕЛ

ИНН 7810962785

erid: 2VSb5zDBFAH

1 000 000 стартапам

Кешбэк 30% на год в Selectel!

Ваш аккаунт

Профиль

Трекер

Диалоги

Настройки

ППА

Разделы

Статьи

Новости

Хабы

Компании

Авторы

Песочница

Информация

Устройство сайта

Для авторов

Для компаний

Документы

Соглашение

Конфиденциальность

Услуги

Корпоративный блог

Медийная реклама

Нативные проекты

Образовательные программы

Стартапам

Настройка языкаТехническая поддержка
© 2006–2025, Habr